package br.com.winesys.geta;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.PersistenceUnit;
import javax.persistence.Query;
import javax.swing.JComponent;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.MaskFormatter;
import javax.validation.constraints.Size;
import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.log4j.Logger;
import br.com.winesys.geta.annotations.Caption;
import br.com.winesys.geta.annotations.Columns;
import br.com.winesys.geta.annotations.Editable;
import br.com.winesys.geta.annotations.EnumSelector;
import br.com.winesys.geta.annotations.LayoutData;
import br.com.winesys.geta.annotations.EnumSelector.SelectorType;
import br.com.winesys.geta.annotations.FileFilter;
import br.com.winesys.geta.annotations.FileFilter.FileChooserAction;
import br.com.winesys.geta.annotations.Format;
import br.com.winesys.geta.annotations.MultiLine;
import br.com.winesys.geta.annotations.Password;
import br.com.winesys.geta.annotations.Uneditable;
import br.com.winesys.geta.editor.AutomatedObjectEditor;
import br.com.winesys.geta.editor.CustomObjectEditor;
import br.com.winesys.geta.editor.Editor;
import br.com.winesys.geta.editor.FieldFactory;
import br.com.winesys.geta.persistence.jpa.JPAEntityManagerFactoryBuilder;

final class ObjectEditorBuilder {

	private static final Logger LOGGER = Logger.getLogger(ObjectEditorBuilder.class);
	
	private static final Map<Class<?>, Integer> defaultLengths = new HashMap<Class<?>, Integer>();

	static {
		defaultLengths.put(String.class, 30);
		defaultLengths.put(Integer.class, 10);
		defaultLengths.put(Long.class, 10);
		defaultLengths.put(Float.class, 10);
		defaultLengths.put(Double.class, 10);
		defaultLengths.put(BigDecimal.class, 10);
	}

	private ObjectEditorBuilder() {
	}

	public static <T> Editor<T> createEditor(final Class<T> entityClass) {
		LOGGER.debug(String.format("Creating editor for %s", entityClass.getName()));
		return new AutomatedObjectEditor<T>() {
			@Override
			protected void createFields() {
				int row = 0;
				for (Field field : entityClass.getDeclaredFields()) {
					LOGGER.debug(String.format("Field found %s", field.getName()));
					if (field.toString().indexOf("static")>=0){
						continue;
					}
					if (field.getAnnotation(Uneditable.class) != null){
						LOGGER.debug("Field uneditable");
						continue;
					}
					Class<?> fieldClass = field.getType();
					if (fieldClass.isEnum()) {
						addField(createEnumEditor(field, row));
					} else if (fieldClass.equals(Integer.class)) {
						addField(createIntegerEditor(field, row));
					} else if (fieldClass.equals(Long.class)) {
						addField(createLongEditor(field, row));
					} else if (fieldClass.equals(Float.class)) {
						addField(createFloatEditor(field, row));
					} else if (fieldClass.equals(Double.class)) {
						addField(createDoubleEditor(field, row));
					} else if (fieldClass.equals(BigDecimal.class)) {
						addField(createBigDecimalEditor(field, row));
					} else if (fieldClass.equals(Date.class)) {
						addField(createDateEditor(field, row));
					} else if (fieldClass.equals(Boolean.class)) {
						addField(createBooleanEditor(field, row));
					} else if (fieldClass.equals(String.class)) {
						addField(createStringEditor(field, row));
					} else if (fieldClass.equals(File.class)) {
						addField(createFileEditor(field, row));
					} else if (fieldClass.equals(List.class)) {
						addField(createListEditor(field, row));
					} else {
						addField(createObjectEditor(field, row));
					}
					row++;
				}
			}
		};
	}

	public static <T> Editor<T> createEditor(final JComponent panelClass) {
		return new CustomObjectEditor<T>(panelClass);
	}

	private static boolean isEditable(Field field) {
		Editable editable = field.getAnnotation(Editable.class);
		if (editable != null) {
			return editable.value();
		}

		return true;
	}

	private static String getName(Field field) {
		return field.getName();
	}

	private static String getDisplay(Field field) {
		Caption caption = field.getAnnotation(Caption.class);
		if (caption != null) {
			return caption.value();
		}
		return WordUtils.capitalize(field.getName());
	}

	private static String getFormat(Field field) {
		Format format = field.getAnnotation(Format.class);
		if (format != null){
			return format.value();
		}
		return "";
	}

	private static int getLength(Field field) {
		Columns column = field.getAnnotation(Columns.class);
		if (column != null){
			return column.value();
		}
		Size size = field.getAnnotation(Size.class);
		if (size != null) {
			return Math.min(defaultLengths.get(field.getType()), size.max());
		}
		return defaultLengths.get(field.getType());
	}

	private static int getLimit(Field field) {
		Size size = field.getAnnotation(Size.class);
		if (size != null) {
			return size.max();
		}
		Column column = field.getAnnotation(Column.class);
		if (column != null) {
			return column.length();
		}
		return 0;
	}

	private static SelectorType getEnumSelectorType(Field field) {
		EnumSelector enumSelector = field.getAnnotation(EnumSelector.class);
		if (enumSelector == null){
			return SelectorType.EnumSelector;
		}
		return enumSelector.value();
	}
	
	private static Object[] getListValues(Field field){
		Type type = field.getGenericType();
			ParameterizedType parameterizedType = (ParameterizedType) type;
		    Class<?> parameterizedClass = (Class<?>) parameterizedType.getActualTypeArguments()[0]; 
			PersistenceUnit pu = parameterizedClass.getAnnotation(PersistenceUnit.class);
			NamedQueries queries = parameterizedClass.getAnnotation(NamedQueries.class);
			NamedQuery listQuery = null;
	        for (NamedQuery query : queries.value()) {
	            if (query.name().equalsIgnoreCase(String.format("%s.listSelectable", parameterizedClass.getSimpleName()))) {
	                listQuery = query;
	            }
	        }
	        if (listQuery == null){
	        	return new Object[]{};
	        }
	        EntityManager manager = JPAEntityManagerFactoryBuilder.getEntityManagerFactory(pu.name()).createEntityManager();
	        Query query = manager.createNamedQuery(listQuery.name());
	        return query.getResultList().toArray();
	}
	
	private static Object getLabelLayout(Field field, int fieldPos){
		LayoutData layout = field.getAnnotation(LayoutData.class);
		if (layout == null){
			return "cell 0 " + fieldPos;
		}
		return layout.caption();
	}
	
	private static Object getEditorLayout(Field field, int fieldPos){
		LayoutData layout = field.getAnnotation(LayoutData.class);
		if (layout == null){
			return "cell 1 " + fieldPos;
		}
		return layout.editor();
	}

	private static br.com.winesys.geta.databinding.Field createEnumEditor(Field field, int row) {
		try {
			Enum<?>[] values = (Enum[]) MethodUtils.invokeStaticMethod(field.getType(), "values", new Object[] {});
			SelectorType enumSelector = getEnumSelectorType(field);
			if (enumSelector.equals(SelectorType.EnumSelector)) {
				return isEditable(field)
						? FieldFactory.createEnumProperty(getName(field), getDisplay(field), values, getLabelLayout(field, row), getEditorLayout(field, row))
						: FieldFactory.createReadOnlyEnumProperty(getName(field), getDisplay(field), values, getLabelLayout(field, row), getEditorLayout(field, row));

			}
			return isEditable(field)
					? FieldFactory.createComboProperty(getName(field), getDisplay(field), values, getLabelLayout(field, row), getEditorLayout(field, row))
					: FieldFactory.createReadOnlyComboProperty(getName(field), getDisplay(field), values, getLabelLayout(field, row), getEditorLayout(field, row));
		} catch (Exception ex) {
			throw new RuntimeException(String.format("Could not load '%s' values", field.getType().getSimpleName()));
		}
	}

	private static br.com.winesys.geta.databinding.Field createIntegerEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createIntegerProperty(getName(field), getDisplay(field), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row))
				: FieldFactory.createReadOnlyIntegerProperty(getName(field), getDisplay(field), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createLongEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createLongProperty(getName(field), getDisplay(field), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row))
				: FieldFactory.createReadOnlyLongProperty(getName(field), getDisplay(field), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createFloatEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createFloatProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), getLabelLayout(field, row), "cell 1 "
						+ row)
				: FieldFactory.createReadOnlyFloatProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), getLabelLayout(field, row),
						getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createDoubleEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createDoubleProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), getLabelLayout(field, row), "cell 1 "
						+ row)
				: FieldFactory.createReadOnlyDoubleProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), getLabelLayout(field, row),
						getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createBigDecimalEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createBigDecimalProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), getLabelLayout(field, row),
						getEditorLayout(field, row))
				: FieldFactory.createReadOnlyBigDecimalProperty(getName(field), getDisplay(field), getFormat(field), getLength(field), "cell 0 "
						+ row, getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createDateEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createDateProperty(getName(field), getDisplay(field), getFormat(field), getLabelLayout(field, row), getEditorLayout(field, row))
				: FieldFactory.createReadOnlyDateProperty(getName(field), getDisplay(field), getFormat(field), getLabelLayout(field, row), getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createStringEditor(Field field, int row) {
		try {
			if (field.getAnnotation(Password.class) != null) {
				return FieldFactory.createPasswordProperty(getName(field), getDisplay(field), getLimit(field), getLength(field), getLabelLayout(field, row),
						getEditorLayout(field, row));
			}
			MultiLine multiLine = field.getAnnotation(MultiLine.class);
			if (multiLine != null) {
				return isEditable(field)
						? FieldFactory.createMultiLineStringProperty(getName(field), getDisplay(field), getLimit(field), getLength(field),
								multiLine.rows(), getLabelLayout(field, row), getEditorLayout(field, row))
						: FieldFactory.createReadOnlyMultiLineStringProperty(getName(field), getDisplay(field), getLength(field), multiLine.rows(),
								getLabelLayout(field, row), getEditorLayout(field, row));
			}
			String format = getFormat(field);
			if (!format.isEmpty()) {
				return isEditable(field)
							? FieldFactory.createFormattedStringProperty(getName(field), getDisplay(field), new DefaultFormatterFactory(
									new MaskFormatter(format)), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row))
							: FieldFactory.createReadOnlyFormattedStringProperty(getName(field), getDisplay(field), new DefaultFormatterFactory(
									new MaskFormatter(format)), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row));
			}
			return isEditable(field)
					? FieldFactory.createStringProperty(getName(field), getDisplay(field), getLimit(field), getLength(field), getLabelLayout(field, row),
							getEditorLayout(field, row))
					: FieldFactory
							.createReadOnlyStringProperty(getName(field), getDisplay(field), getLength(field), getLabelLayout(field, row), getEditorLayout(field, row));
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	private static br.com.winesys.geta.databinding.Field createBooleanEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createBooleanProperty(getName(field), getDisplay(field), getEditorLayout(field, row))
				: FieldFactory.createReadOnlyBooleanProperty(getName(field), getDisplay(field), getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createFileEditor(Field field, int row) {
		FileFilter fileFilter = field.getAnnotation(FileFilter.class);
		boolean openDialog = true;
		String name = "";
		String[] extensions = { "*" };
		if (fileFilter != null) {
			openDialog = fileFilter.type() == FileChooserAction.Open;
			name = fileFilter.name();
			extensions = fileFilter.extensions();
		}
		return openDialog
				? FieldFactory.createFileOpenProperty(getName(field), getDisplay(field), new FileNameExtensionFilter(name, extensions),
						getLabelLayout(field, row), getEditorLayout(field, row))
				: FieldFactory.createFileSaveProperty(getName(field), getDisplay(field), new FileNameExtensionFilter(name, extensions),
						getLabelLayout(field, row), getEditorLayout(field, row));
	}

	private static br.com.winesys.geta.databinding.Field createObjectEditor(Field field, int row) {
		return isEditable(field)
				? FieldFactory.createObjectProperty(getName(field), getDisplay(field), GeTA.createObjectSelector(field.getType()), getLabelLayout(field, row),
						getEditorLayout(field, row))
				: FieldFactory.createReadOnlyObjectProperty(getName(field), getDisplay(field), GeTA.createObjectSelector(field.getType()), "cell 0 "
						+ row, getEditorLayout(field, row));
	}
	
	private static br.com.winesys.geta.databinding.Field createListEditor(Field field, int row) {
		return FieldFactory.createListProperty(getName(field), getDisplay(field), getListValues(field), getLabelLayout(field, row), getEditorLayout(field, row) + ", h 160!");
	}
}
